﻿--[[
	Copyright (c) 2012 Eyal Shilony

	Permission is hereby granted, free of charge, to any person obtaining
	a copy of this software and associated documentation files (the
	"Software"), to deal in the Software without restriction, including
	without limitation the rights to use, copy, modify, merge, publish,
	distribute, sublicense, and/or sell copies of the Software, and to
	permit persons to whom the Software is furnished to do so, subject to
	the following conditions:

	The above copyright notice and this permission notice shall be
	included in all copies or substantial portions of the Software.

	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
	EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
	MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
	NONINFRINGEMENT. IN 
	
	NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
	LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
	OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
	WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

	-- A lightweight library that provides a common infrastructure for addons.
]]

local _, addon = ...

local modules = { }
local events = { }
local methods = { }
local timers = { }
local heap = { }

local frame = CreateFrame("Frame")
frame:RegisterEvent("ADDON_LOADED")
frame:RegisterEvent("PLAYER_LOGIN")
frame:RegisterEvent("PLAYER_LOGOUT")

--[[ Localization ]]

local L = {
	["BAD_ARGUMENT"] = "bad argument #%d to '%s' (%s expected, got %s)",
	["MALFORMED_PATTERN"] = "'malformed pattern in '%s'.",
	["MODULE_ALREADY_DEFINED"] = "the module '%s' is already defined.",
	["MODULE_NOT_DEFINED"] = "the module '%s' is not defined.",
	["EVENT_ALREADY_REGISTERED"] = "the event '%s' is already registered.",
	["EVENT_HANDLER_NOT_FOUND"] = "event handler was not found for '%s'.",
	["EVENT_CANNOT_BE_TRIGGERED"] = "the event '%s' is a built-in event and cannot be triggered manually.",
	["EVENT_CANNOT_BE_REGISTERED"] = "the event '%s' cannot be registered, use the built-in event '%s'.",
	["SCHEDULE_DELAY_LESS_THAN_ZERO"] = "schedule delay time is less than zero.",
}

--[[ Library Helper Methods ]]

local function assert(level, condition, message, ...)
	if condition then
		error(message:format(...), level)
	end
end

local function argcheck(value, num, ...)
	if type(num) ~= "number" then
		error(L["BAD_ARGUMENT"]:format(2, "argcheck", "number", type(num)), 1)
	end

	for i = 1, select("#", ...) do
		if type(value) == select(i, ...) then return end
	end

	local types = strjoin(", ", ...)
	local name = string.match(debugstack(2, 2, 0), ": in function [`<](.-)['>]")
	error(L["BAD_ARGUMENT"]:format(num, name, types, type(value)), 3)
end

local function safecall(func, ...)
	local success, err = pcall(func, ...)
	if not success then
		geterrorhandler()(err)
	end
end

--[[ Events Management ]]

local function fire(events, event, ...)
	if events and type(events) == "table" then
		for module, func in pairs(events) do
			if type(func) == "string" then
				assert(3, not module[func], L["EVENT_HANDLER_NOT_FOUND"], event)
				if type(module[func]) == "function" then
					safecall(module[func], module, event, ...)
				end
			else
				safecall(func, module, event, ...)
			end
		end
	end
end

--[[ RegisterEvent(event[, func]) - Registers an event.

	Arguments.
	event - (string) The event to register.
	func - (func) The event handler method to handle the event.
	
	Usage.
	local _, addon = ...
	local core = addon:Create()
	
	core:RegisterEvent("PLAYER_LOGIN", function(self, event)
	end)
	
	Registers a custom event.
	
	core:RegisterEvent("SOME_EVENT", function(self, event)
	end)
	
]]
function methods:RegisterEvent(event, func)
	argcheck(event, 2, "string")
	argcheck(func, 3, "function", "nil")
	assert(3, events[event] and events[event][self], L["EVENT_ALREADY_REGISTERED"], event)
	assert(3, event == "ADDON_LOADED", L["EVENT_CANNOT_BE_REGISTERED"], event, "ADDON_INIT")
	assert(3, event == "PLAYER_LOGIN", L["EVENT_CANNOT_BE_REGISTERED"], event, "ADDON_ENABLED")
	assert(3, event == "PLAYER_LOGOUT", L["EVENT_CANNOT_BE_REGISTERED"], event, "ADDON_DISABLED")
	func = func or event
	if not events[event] then
		events[event] = { }
		frame:RegisterEvent(event)
	end
	events[event][self] = func
end

--[[ RegisterEvents(...[, func]) - Registers events.

	Arguments.
	args - (table) The list of events to register.
	func - (func) The event handler method to handle the events. (this should always be the last argument you provide.)
	
	Usage.
	local _, addon = ...
	local core = addon:Create()
	
	core:RegisterEvents("PLAYER_LOGIN", "PLAYER_ENTERING_WORLD", function(self, event)
	end)
	
	Registers custom events.
	
	core:RegisterEvents("SOME_EVENT1", "SOME_EVENT2", function(self, event)
	end)
	
]]
function methods:RegisterEvents(...)
	local event
	local count = select("#", ...)
	local lastArg = select(count, ...)
	if type(lastArg) ~= "function" then
		lastArg = nil
	end
	for i = 1, count do
		if not lastArg or (lastArg and i <= count - 1) then
			event = select(i, ...)
			methods.RegisterEvent(self, event, lastArg)
		end
	end
end

--[[ UnregisterEvent(event) - Unregisters an event.

	Arguments.
	event - (string) The event to unregister.
	
	Usage.
	local _, addon = ...
	local core = addon:Create()
	
	core:UnregisterEvent("PLAYER_LOGIN")
	
	Unregisters a custom event.
	
	core:UnregisterEvent("SOME_EVENT")
	
]]
function methods:UnregisterEvent(event)
	argcheck(event, 2, "string")
	local e = events[event]
	if e then
		e[self] = nil
		if not next(e) then
			events[event] = nil
			frame:UnregisterEvent(event)
		end
	end
end

--[[ UnregisterEvents(...) - Unregisters events.

	Arguments.
	args - (table) The list of events to unregister.
	
	Usage.
	local _, addon = ...
	local core = addon:Create()
	
	core:UnregisterEvents("PLAYER_LOGIN", "PLAYER_ENTERING_WORLD")
	
	Unregisters custom events.
	
	core:UnregisterEvents("SOME_EVENT1", "SOME_EVENT2")
	
]]
function methods:UnregisterEvents(...)
	local event
	for i = 1, select("#", ...) do
		event = select(i, ...)
		methods.UnregisterEvent(self, event)
	end
end

--[[ IsEventRegistered(event) - Determines whether an event is registered.

	Arguments.
	event - (string) The event to check whether is registered.
	
	Returns.
	registered - (boolean) true if registered; otherwise, false.
	
	Usage.
	local _, addon = ...
	local core = addon:Create()
	
	core:IsEventRegistered("PLAYER_LOGIN")
	
	Determines whether a custom event is registered.
	
	core:IsEventRegistered("SOME_EVENT")
	
]]
function methods:IsEventRegistered(event)
	argcheck(event, 2, "string")
	local e = events[event]
	if e and e[self] then
		return true
	else
		return false
	end
end

--[[ TriggerEvent(event, ...) - Triggers a custom event.

	Arguments.
	event - (string) The event to trigger.
	args - (table) An arbitrarily list of arguments.
	
	Usage.
	local _, addon = ...
	local core = addon:Create()
	
	Triggers a custom event.
	
	core:TriggerEvent("SOME_EVENT")
	
]]
local function triggerEvent(event, isBuiltIn, ...)
	assert(3, not isBuiltIn and (event == "ADDON_INIT" or event == "ADDON_ENABLED" or event == "ADDON_DISABLED"), L["EVENT_CANNOT_BE_TRIGGERED"], event)
	fire(events[event], event, ...)
end

function methods:TriggerEvent(event, ...)
	argcheck(event, 2, "string")
	triggerEvent(event, false, ...)
end

--[[ Task Scheduling ]]

local function heapSwap(i1, i2)
	heap[i1], heap[i2] = heap[i2], heap[i1]
end

local function heapBubbleUp(index)
	while index > 1 do
		local parentIndex = math.floor(index / 2)
		if heap[index].interval < heap[parentIndex].interval then
			heapSwap(index, parentIndex)
			index = parentIndex
		else
			break
		end
	end
end

local function heapBubbleDown(index)
	while 2 * index <= heap.lastIndex do
		local leftIndex = 2 * index
		local rightIndex = leftIndex + 1
		local current = heap[index]
		local leftChild = heap[leftIndex]
		local rightChild = heap[rightIndex]
		if not rightChild then
			if leftChild.interval < current.interval then
				heapSwap(index, leftIndex)
				index = leftIndex
			else
				break
			end
		else
			if leftChild.interval < current.interval or
			   rightChild.interval < current.interval then
				if leftChild.interval < rightChild.interval then
					heapSwap(index, leftIndex)
					index = leftIndex
				else
					heapSwap(index, rightIndex)
					index = rightIndex
				end
			else
				break
			end
		end
	end
end

local function cancelTimer(self)
	self.cancelled = true
end

local function timer(schedule, delay, func, ...)
	argcheck(schedule, 1, "table")
	
	timers[schedule] = func
	schedule.interval = GetTime() + delay
	
	if select('#', ...) ~= 0 then
		schedule.args = { ... }
	end

	if heap.lastIndex then
		heap.lastIndex = heap.lastIndex + 1
	else
		heap.lastIndex = 1
	end
	
	heap[heap.lastIndex] = schedule
	heapBubbleUp(heap.lastIndex)
	
	if not frame:IsShown() then
		frame:Show()
	end
	
	return schedule
end

--[[ CreateTask(delay, func, ...) - Creates a one time task.

	Arguments.
	delay - (number) The time interval to fire the task.
	func - (function) The function to be fired.
	args - (table) An arbitrarily list of arguments.
	
	Returns.
	task - (table) A task.
	
	Usage.
	local _, addon = ...
	local core = addon:Create()
	
	local task = core:CreateTask(0.1, function(self)
	end)
	
	To cancel a task use the cancel method like so.
	
	task:Cancel()
	
]]
function methods:CreateTask(delay, func, ...)
	argcheck(delay, 2, "number")
	argcheck(func, 3, "function")
	assert(3, delay < 0, L["SCHEDULE_DELAY_LESS_THAN_ZERO"])
	local schedule = { }
	schedule.module = self
	schedule.Cancel = cancelTimer
	return timer(schedule, delay, func, ...)
end

--[[ ScheduleTask(delay, func, ...) - Schedules a repetitive task.

	Arguments.
	delay - (number) The time interval to fire the task.
	func - (function) The function to be fired.
	args - (table) An arbitrarily list of arguments.
	
	Returns.
	task - (table) A scheduled task.
	
	Usage.
	local _, addon = ...
	local core = addon:Create()
	
	local task = core:ScheduleTask(0.1, function(self)
	end)
	
	To cancel a task use the cancel method like so.
	
	task:Cancel()
	
]]
function methods:ScheduleTask(delay, func, ...)
	local schedule = methods.CreateTask(self, delay, func, ...)
	schedule.delay = delay
	return schedule
end

--[[ Modules Management ]]

--[[ NewModule(name) - Creates new module.

	Arguments.
	name - (string) The name of the module.
	
	Returns.
	module - (table) A module.
	
]]
function addon:NewModule(name)
	argcheck(name, 2, "string")
	local module = modules[name] or { }
	assert(2, module[name], L["MODULE_ALREADY_DEFINED"], name)
	module.name = name
	setmetatable(module, { __index = methods })
	modules[name] = module
	return module 
end

--[[
 GetModule(name) - Gets the given module.

	Arguments.
	name - (string) The name of the module.
	
	Returns.
	module - (table) A module.
	
]]
function addon:GetModule(name)
	argcheck(name, 2, "string")
	local module = modules[name]
	assert(2, not module, L["MODULE_NOT_DEFINED"], name)
	return module
end

--[[ IsModuleRegistered(event) - Determines whether the module is registered.

	Arguments.
	name - (string) The name of the module.
	
	Returns.
	registered - (boolean) true if registered; otherwise, false.
	
]]
function addon:IsModuleRegistered(name)
	argcheck(name, 2, "string")
	local module = modules[name]
	if module then
		return true
	else
		return false
	end
end

--[[ Modules() - Traverses through all modules. ]]
function addon:Modules()
	return pairs(modules)
end

--[[ Library Internal Event Handlers ]]

function frame:ADDON_LOADED(event, name)
	triggerEvent("ADDON_INIT", true)
end

function frame:PLAYER_LOGIN(event)
	triggerEvent("ADDON_ENABLED", true)
end

function frame:PLAYER_LOGOUT(event)
	triggerEvent("ADDON_DISABLED", true)
end

local function onEvent(self, event, ...)
	if frame[event] then
		frame[event](self, event, ...)
		return
	end
	fire(events[event], event, ...)
end

local function onUpdate(self, elapsed)
	local schedule = heap[1]
	while schedule and schedule.interval < GetTime() do
		if schedule.cancelled then
			heapSwap(1, heap.lastIndex)
			heap[heap.lastIndex] = nil
			heap.lastIndex = heap.lastIndex - 1
			timers[schedule] = nil
		else
			local func = timers[schedule]
			if schedule.args then
				safecall(func, schedule.module, unpack(schedule.args))
			else
				safecall(func, schedule.module)
			end
			if schedule.delay then
				schedule.interval = GetTime() + schedule.delay
			else
				schedule:Cancel()
			end
		end
		heapBubbleDown(1)
		schedule = heap[1]
	end
	if not schedule then self:Hide() end
end

frame:SetScript("OnEvent", onEvent)
frame:SetScript("OnUpdate", onUpdate)

--[[ Library Initialization ]]

--[[ Create() - Creates the main module for the current addon.
	
	Returns.
	core - (table) The instance of the module.
	
	Usage.
	local _, addon = ...
	local core = addon:Create()

]]
function addon:Create()
	return addon:NewModule("Core")
end